import _ from 'lodash';
import * as Sys from './sys';
import * as SysBom from './sys.bom';

enum HttpMethod {
    Get = 1,
    Put,
    Post,
    Delete,
    Upload,
};

enum AjaxStatus {
    // 请求被初始化。
    Init = 1,
    // 请求已发送。
    Sent,
    // 服务器返回了合法的响应（可能表示失败的业务。）
    Resolved,
    // 服务器返回了不合法的响应（4xx错误、5xx错误或者响应无法被解析为json。）
    Rejected,
    // 请求无法被发送到服务器。
    Cancelled
}

interface IRejectReason {
    url: string,
    code: number,
    extras: any,
}

type ResolveCallback = (value: {}) => void;
type RejectCallback = (value: IRejectReason) => void;

const KEY_WORD_PATTERN = /([a-z0-9])([A-Z])/g;

const regularParamKey = (k: string): string =>
    k.replace(KEY_WORD_PATTERN, (m, p1, p2) => p1 + '-' + p2.toLowerCase()).toLowerCase();

const regularParamValue = (v?: any): string => {
    if (_.isNil(v)) {
        return '';
    } else if (_.isNumber(v)) {
        return _.isFinite(v) ? _.isSafeInteger(v) ? v.toFixed(0) : (v < 1 && v > -1) ? v.toFixed(4) : v.toFixed(2) : '';
    } else if (_.isBoolean(v)) {
        return v ? '1' : '0';
    } else if (_.isDate(v)) {
        return v.getTime().toFixed(0);
    } else if (_.isString(v)) {
        return v;
    } else if (_.isArrayLike(v)) {
        return _.map(v, item => regularParamValue(item)).join(',');
    } else if (_.isFunction(v)) {
        return regularParamValue(v.call(null));
    } else if (_.isRegExp(v)) {
        throw new TypeError('Cannot urlencode Regexp ' + v);
    } else if (_.isError(v)) {
        throw new TypeError('Cannot urlencode Error ' + v);
    } else {
        throw new TypeError('Cannot urlencode Object ' + v);
    }
};

/**
 * 表示Ajax调用的Promise。
 */
class AjaxPromise {
    private method: HttpMethod;
    private url: string;
    private data: URLSearchParams | FormData;
    private status: AjaxStatus;
    private resolve: ResolveCallback;
    private reject: RejectCallback;

    constructor(method: HttpMethod, url: string, params: any, data: any, resolve: ResolveCallback, reject: RejectCallback) {
        this.method = method;

        if (url.lastIndexOf('?') !== -1) {
            throw new Error('URL must not contains "?", use params instead');
        }

        if (!_.isEmpty(params)) {
            const uq: URLSearchParams = new URLSearchParams();
            _.forOwn(params, (value, key) => {
                const sk = regularParamKey(key);
                const sv = regularParamValue(value);
                if (sv && sk) {
                    uq.append(sk, sv);
                }
            });
            this.url = url + '?' + uq.toString();
        } else {
            this.url = url;
        }

        if (!_.isEmpty(data)) {
            if (method === HttpMethod.Post || method === HttpMethod.Put) {
                const uq: URLSearchParams = new URLSearchParams();
                _.forOwn(data, (value, key) => {
                    const sk = regularParamKey(key);
                    const sv = regularParamValue(value);
                    if (sv && sk) {
                        uq.append(sk, sv);
                    }
                });
                this.data = uq;
            } else if (method === HttpMethod.Upload) {
                const fd: FormData = new FormData();
                _.forOwn(data, (value, key) => {
                    const sk = regularParamKey(key);
                    const sv = regularParamValue(value);
                    if (sv && sk) {
                        fd.append(sv, sk);
                    }
                });
                this.data = fd;
            } else {
                this.data = null;
                console.warn('Body ignored', data);
            }
        } else {
            this.data = null;
        }

        this.resolve = resolve;
        this.reject = reject;
        this.status = AjaxStatus.Init;
    }

    toString(): string {
        return 'AjaxPromise{url=' + this.url + ',status=' + this.status + '}';
    }

    /**
     * 执行Ajax调用。
     */
    call(): void {
        if (this.status !== AjaxStatus.Init) {
            throw new Error('Illegal status: ' + this.status);
        }

        // 设置标志，Ajax操作已开始。
        if (globalAjaxRunning) {
            throw new Error('A running ajax-promise already exists');
        }
        globalAjaxRunning = true;

        window.fetch(
            this.url,
            {
                body: this.data,
                credentials: 'same-origin',
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': this.method === HttpMethod.Post || this.method === HttpMethod.Put ? 'application/x-www-form-urlencoded' :
                        this.method === HttpMethod.Upload ? 'multipart/form-data' : null,
                },
                method: AjaxPromise.getMethodName(this.method),
                mode: 'cors',
                redirect: 'follow',
            })
            .then((response: Response) => {
                const urlString = '(url=' + this.url + ')';
                if (response.ok) {
                    const contentType: string = response.headers.get('Content-Type');

                    if (contentType === null || contentType.startsWith('application/json')) {
                        // 如果未指定返回值或者返回值是json，那么尝试解析。
                        response.json().then(data => {
                            // 如果解析成功，那么触发成功回调。
                            this.status = AjaxStatus.Resolved;
                            this.resolve(data);
                        }).catch((err: any) => {
                            // 如果解析失败，那么记录错误。
                            this.status = AjaxStatus.Rejected;
                            console.error('Parse json(ok) failed: ', err, urlString);
                        });
                    } else {
                        // 如果返回值不是json，那么记录错误。
                        this.status = AjaxStatus.Rejected;
                        console.error('Unexpected content type:', contentType, urlString);
                    }
                } else {
                    if (response.status === 500) {
                        // 如果服务器返回了500错误，那么尝试解析详细内容。 那么记录错误，然后触发错误回调。
                        response.json().then((data: any) => {
                            // 如果解析成功，那么记录错误。
                            this.status = AjaxStatus.Rejected;
                            if (data) {
                                const ess: string[] = ['Server error'];
                                if (data.timestamp) {
                                    ess.push('timestamp: ' + new Date(data.timestamp));
                                }
                                if (data.path) {
                                    ess.push('path: ' + data.path);
                                }
                                if (data.trace) {
                                    ess.push('---- stack trace ---');
                                    _.each(data.trace, te => ess.push(te));
                                    ess.push('--------------------');
                                } else if (data.message) {
                                    ess.push('message: ' + data.message);
                                }
                                console.error(_.join(ess, '\n'));

                                // 如果解析结果中包含有效的错误码，那么触发回调。
                                const code = data.code || 0;
                                if (code !== 0) {
                                    this.reject({ url: this.url, code, extras: data.extras || {} });
                                }
                            } else {
                                console.error('Result of json(' + response.status + ') is null', urlString);
                            }
                        }).catch(err => {
                            // 如果解析失败，那么记录错误。
                            this.status = AjaxStatus.Rejected;
                            console.error('Parse json(' + response.status + ') failed: ', err, urlString);
                        });
                    } else if (response.status === 401) {
                        // 如果服务器返回了401错误，那么记录错误，并触发自动跳转。
                        this.status = AjaxStatus.Rejected;
                        console.error('Authentication required', urlString);
                        const current_: string = (window as any).env.current;
                        const name_: string = (window as any).env.name;
                        let fullName = current_.endsWith('/') ? current_ + name_ : current_ + '/' + name_;
                        if (location.search) { fullName += location.search; }
                        const safeFullName = Sys.urlSafeBtoa(fullName);
                        SysBom.jumpTo('/sign-in?return=' + safeFullName);
                    } else {
                        // 如果服务器返回了其它错误，那么不执行任何操作。
                        this.status = AjaxStatus.Rejected;
                    }
                }
            })
            .catch((err: any) => {
                // 如果无法获取响应，那么不执行任何操作。
                this.status = AjaxStatus.Cancelled;
            })
            .finally(() => {
                // 设置标志，Ajax操作已完成。
                globalAjaxRunning = false;
                ajaxCallNext();
            });

        this.status = AjaxStatus.Sent;
    }

    private static getMethodName(method: HttpMethod): string {
        switch (method) {
            case HttpMethod.Get:
                return 'GET';
            case HttpMethod.Put:
                return 'PUT';
            case HttpMethod.Post:
                return 'POST';
            case HttpMethod.Delete:
                return 'DELETE';
            default:
                throw new Error('Unknown method: ' + method);
        }
    }
}

const globalAjaxQueue: AjaxPromise[] = [];
let globalAjaxRunning: boolean = false;

const ajax = (method: HttpMethod, url: string, params: any, data: any): Promise<any> => {
    return new Promise<any>((resolve, reject) => {
        const ap: AjaxPromise = new AjaxPromise(method, url, params, data, resolve, reject);

        globalAjaxQueue.push(ap);

        if (!globalAjaxRunning) {
            // 如果当前不存在未完成的Ajax调用，那么从队列中取出第一个调用并执行。
            ajaxCallNext();
        }
    });
};

const ajaxCallNext = () => {
    const a: AjaxPromise = globalAjaxQueue.shift();
    if (a !== undefined) {
        a.call();
    }
}

export const ajaxIsBusy = () => globalAjaxRunning || globalAjaxQueue.length !== 0;

export const ajaxGet = (url: string, params?: {}): Promise<any> => {
    return ajax(HttpMethod.Get, url, params || {}, {});
};

export const ajaxPut = (url: string, params?: {}, data?: {}): Promise<any> => {
    return ajax(HttpMethod.Put, url, params || {}, data || {});
};

export const ajaxPost = (url: string, params?: {}, data?: {}): Promise<any> => {
    return ajax(HttpMethod.Post, url, params || {}, data || {});
};

export const ajaxDelete = (url: string, params?: {}): Promise<any> => {
    return ajax(HttpMethod.Delete, url, params || {}, {});
};

export const apiUrl = (url: string): string => {
    url = url.trim();
    if (!url.startsWith('/')) {
        url = '/' + url;
    }

    const api: string = (window as any).env.api!;
    return api + url;
};

export type ApiFunction = ({ }) => Promise<any>;

const ApiFunctions: Map<string, ApiFunction> = new Map();
const ApiResults: Map<string, any> = new Map();

export const register = (name: string, apiFunc: ApiFunction): void => {
    name = name.trim();

    if (ApiFunctions.has(name)) {
        throw new Error('Duplicated api: ' + name);
    }

    ApiFunctions.set(name, apiFunc);
    ApiResults.delete(name);
};

export const dumpAll = (): void => {
    const o: any = {};

    _.each(ApiFunctions, (fn, name) =>
        o[name] = ApiResults.has(name) ? ApiResults.get(name) : undefined
    );

    console.debug('All registered api: ', o);
};

export const call = async (name: string, params: any = {}, force: boolean = false): Promise<any> => {
    name = name.trim();

    if (force && ajaxIsBusy()) {
        return Promise.reject({ url: '', code: -2, text: 'Ajax is busy' });
    }

    if (!force && ApiResults.has(name)) {
        return Promise.resolve(ApiResults.get(name));
    } else if (ApiFunctions.has(name)) {
        return ApiFunctions.get(name).call(window, params).then((data: any) => {
            ApiResults.set(name, data);
            return data;
        });
    } else {
        throw new Error('Unknown api: ' + name);
    }
};

export interface IPageArgs {
    index?: number,
    pageSize?: number,
}

export const page = (pageArgs: IPageArgs): string => {
    if (_.isNil(pageArgs) || _.isNil(pageArgs.index) || _.isNil(pageArgs.pageSize)) {
        return '';
    }

    return '' + ((pageArgs.index - 1) * pageArgs.pageSize) + ',' + pageArgs.pageSize;
}

if (typeof process !== 'object' || (process as any).browser) {
    console.info('%cSys.api module (2.1.0) loaded', 'font-weight:bold;color: #4baf50;');
}